package com.google.android.apps.common.testing.ui.espresso.base; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Throwables.propagate; import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException; import android.os.Build; import android.util.Log; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyEvent; import android.view.MotionEvent; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * An {@link EventInjectionStrategy} that uses the input manager to inject Events. * This strategy supports API level 16 and above. */ final class InputManagerEventInjectionStrategy implements EventInjectionStrategy { private static final String TAG = InputManagerEventInjectionStrategy.class.getSimpleName(); // Used in reflection private boolean initComplete; private Method injectInputEventMethod; private Method setSourceMotionMethod; private Object instanceInputManagerObject; private int motionEventMode; private int keyEventMode; InputManagerEventInjectionStrategy() { checkState(Build.VERSION.SDK_INT >= 16, "Unsupported API level."); } void initialize() { if (initComplete) { return; } try { Log.d(TAG, "Creating injection strategy with input manager."); // Get the InputputManager class object and initialize if necessary. Class<?> inputManagerClassObject = Class.forName("android.hardware.input.InputManager"); Method getInstanceMethod = inputManagerClassObject.getDeclaredMethod("getInstance"); getInstanceMethod.setAccessible(true); instanceInputManagerObject = getInstanceMethod.invoke(inputManagerClassObject); injectInputEventMethod = instanceInputManagerObject.getClass() .getDeclaredMethod("injectInputEvent", InputEvent.class, Integer.TYPE); injectInputEventMethod.setAccessible(true); // Setting event mode to INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH to ensure // that we've dispatched the event and any side effects its had on the view hierarchy // have occurred. Field motionEventModeField = inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH"); motionEventModeField.setAccessible(true); motionEventMode = motionEventModeField.getInt(inputManagerClassObject); Field keyEventModeField = inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH"); keyEventModeField.setAccessible(true); keyEventMode = keyEventModeField.getInt(inputManagerClassObject); setSourceMotionMethod = MotionEvent.class.getDeclaredMethod("setSource", Integer.TYPE); InputEvent.class.getDeclaredMethod("getSequenceNumber"); initComplete = true; } catch (ClassNotFoundException e) { propagate(e); } catch (IllegalAccessException e) { propagate(e); } catch (IllegalArgumentException e) { propagate(e); } catch (InvocationTargetException e) { propagate(e); } catch (NoSuchMethodException e) { propagate(e); } catch (SecurityException e) { propagate(e); } catch (NoSuchFieldException e) { propagate(e); } } @Override public boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException { try { return (Boolean) injectInputEventMethod.invoke(instanceInputManagerObject, keyEvent, keyEventMode); } catch (IllegalAccessException e) { propagate(e); } catch (IllegalArgumentException e) { propagate(e); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof SecurityException) { throw new InjectEventSecurityException(cause); } propagate(e); } catch (SecurityException e) { throw new InjectEventSecurityException(e); } return false; } @Override public boolean injectMotionEvent(MotionEvent motionEvent) throws InjectEventSecurityException { try { // Need to set the event source to touch screen, otherwise the input can be ignored even // though injecting it would be successful. // TODO(user): proper handling of events from a trackball (SOURCE_TRACKBALL) and joystick. if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0 && !isFromTouchpadInGlassDevice(motionEvent)) { // Need to do runtime invocation of setSource because it was not added until 2.3_r1. setSourceMotionMethod.invoke(motionEvent, InputDevice.SOURCE_TOUCHSCREEN); } return (Boolean) injectInputEventMethod.invoke(instanceInputManagerObject, motionEvent, motionEventMode); } catch (IllegalAccessException e) { propagate(e); } catch (IllegalArgumentException e) { propagate(e); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof SecurityException) { throw new InjectEventSecurityException(cause); } propagate(e); } catch (SecurityException e) { throw new InjectEventSecurityException(e); } return false; } // We'd like to inject non-pointer events sourced from touchpad in Glass. private static boolean isFromTouchpadInGlassDevice(MotionEvent motionEvent) { return (Build.DEVICE.contains("glass") || Build.DEVICE.contains("Glass") || Build.DEVICE.contains("wingman")) && ((motionEvent.getSource() & InputDevice.SOURCE_TOUCHPAD) != 0); } }